package com.vincent.suchao.util;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PointF;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.view.GestureDetector;
import android.view.GestureDetector.OnDoubleTapListener;
import android.view.GestureDetector.OnGestureListener;
import android.view.MotionEvent;
import android.view.ScaleGestureDetector;
import android.view.ScaleGestureDetector.OnScaleGestureListener;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.DecelerateInterpolator;

/**
 * better image view, like the imageview, but support multi-touch and zoom in and out. </br><strong>Notes:
 * some function has some limit</strong>
 * 
 * @author idiottiger
 * @version 1.0
 * 
 */
public class BetterImageView extends View implements OnScaleGestureListener, OnGestureListener,
		OnDoubleTapListener {
	private MyCallback callback;

	public static final String LOG_TAG = "BetterImageView";

	private final Paint mPaint = new Paint(Paint.FILTER_BITMAP_FLAG);

	static final int MAX_MOVE_P_X = 400, MAX_MOVE_P_Y = 40;

	static final int DEFAULT_DURATION = 150, SMALL_DURATION = 50;

	private float mScaleFactor = 1.f;

	private Bitmap mBitmap;

	private Matrix matrix = new Matrix(), savedMatrix = new Matrix();

	private boolean isInited;

	private float mPosX, mPosY;

	@SuppressWarnings("unused")
	private float leftX, rightX;

	private float minScale, maxScale, defaultScale;

	private int canvasWidth, canvasHeight, bitmapWidth, bitmapHeight;

	// We can be in one of these 3 states
	static final int NONE = 0, DRAG = 1, ZOOM = 2;

	private int mode = NONE;

	// Remember some things for zooming
	PointF startPoint = new PointF();

	private ScaleGestureDetector mScaleDetector;

	private GestureDetector mDetector;

	private boolean isScaling, isMovingUp, isScaleAnimCanStarted;

	private AbsoluteTranslateAnimation mTranslateAnimation;

	private AbsoluteScaleAnimation mScaleAnimation;

	private AnimationListener mAnimationListener;

	float[] src = new float[2];

	float[] dst = new float[2];

	/**
	 * 
	 * @param context
	 */
	public BetterImageView(Context context) {
		this(context, null);
	}

	/**
	 * can set in the xml file, but not supprot {@link View} attrs, not imageview.
	 * 
	 * @param context
	 * @param attrs
	 */
	public BetterImageView(Context context, AttributeSet attrs) {
		super(context, attrs);
		mScaleDetector = new ScaleGestureDetector(context, this);
		mDetector = new GestureDetector(context, this);
		mDetector.setOnDoubleTapListener(this);
		mAnimationListener = new TranslateAnimationListener();

	}

	/**
	 * set bitmap tp image view
	 * 
	 * @param map
	 */
	public void setBitmap(Bitmap map) {
		reset();
		mBitmap = map;
		postInvalidate();
	}

	/**
	 * return bitmap in the imageview
	 * 
	 * @return
	 */
	public Bitmap getBitmap() {
		return mBitmap;
	}

	@Override
	protected void onDraw(Canvas canvas) {
		if (isBitmapAvailable()) {
			init(canvas);
			updateAnimation();
			drawDrawable(canvas);
			checkAnimation();
		}
	}

	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);

		canvasWidth = MeasureSpec.getSize(widthMeasureSpec);
		canvasHeight = MeasureSpec.getSize(heightMeasureSpec);
		isInited = false;
	}

	/**
	 * reset the image view's all states, the bitmap will be recycled.
	 */
	public void reset() {
		// recycle the bitmap
		if (isBitmapAvailable()) {
			mBitmap.recycle();
			mBitmap = null;
		}
		if (matrix != null) {
			matrix.reset();
		}
		if (savedMatrix != null) {
			savedMatrix.reset();
		}
		if (mTranslateAnimation != null) {
			mTranslateAnimation.reset();
		}
		if (mScaleAnimation != null) {
			mScaleAnimation.reset();
		}
		isInited = false;
	}

	/**
	 * update view, if the bitmap change or the ori change
	 */
	protected void updateView() {
		makeViewLaterRefresh();
		postInvalidate();
	}

	private void makeViewLaterRefresh() {
		if (matrix != null) {
			matrix.reset();
		}
		if (savedMatrix != null) {
			savedMatrix.reset();
		}
		isInited = false;
	}

	private void drawDrawable(Canvas canvas) {
		if (isBitmapAvailable()) {
			canvas.drawBitmap(mBitmap, matrix, mPaint);
		}
	}

	/**
	 * check the bitmap can drawable
	 * 
	 * @return true or false
	 */
	public boolean isBitmapAvailable() {
		return mBitmap != null && !mBitmap.isRecycled();
	}

	private void init(Canvas canvas) {
		if (!isInited) {

			// get w, h
			bitmapWidth = mBitmap.getWidth();
			bitmapHeight = mBitmap.getHeight();

			// get default scale
			float scale;
			if (isError) {
				scale = 1;
			} else {
				scale = Math.min((float) canvasWidth / bitmapWidth, (float) canvasHeight / bitmapHeight);
			}
			mScaleFactor = scale;
			defaultScale = scale;

			minScale = mScaleFactor;
			maxScale = mScaleFactor * 3;

			mPosX = (canvasWidth - scale * bitmapWidth) / 2 / scale;
			mPosY = (canvasHeight - scale * bitmapHeight) / 2 / scale;

			if (matrix != null) {
				matrix.reset();
				savedMatrix.reset();
			} else {
				matrix = new Matrix();
				savedMatrix = new Matrix();
			}
			updateMatrix();
			isInited = true;
		}
	}

	private boolean isError;

	public void setError(boolean isErrorTemp) {
		isError = isErrorTemp;
		if (mBitmap != null) {
			// get w, h
			bitmapWidth = mBitmap.getWidth();
			bitmapHeight = mBitmap.getHeight();

			// get default scale
			float scale;
			if (isError) {
				scale = 1;
			} else {
				scale = Math.min((float) canvasWidth / bitmapWidth, (float) canvasHeight / bitmapHeight);
			}
			System.out.println(" scale " + scale);
			mScaleFactor = scale;
			defaultScale = scale;

			minScale = mScaleFactor;
			maxScale = mScaleFactor * 3;

			mPosX = (canvasWidth - scale * bitmapWidth) / 2 / scale;
			mPosY = (canvasHeight - scale * bitmapHeight) / 2 / scale;
			invalidate();
		}

	}

	private void updateMatrix() {
		matrix.setTranslate(mPosX, mPosY);
		matrix.postScale(mScaleFactor, mScaleFactor);
		savedMatrix.reset();
		savedMatrix.set(matrix);
	}

	@Override
	public boolean onTouchEvent(MotionEvent event) {

		mScaleDetector.onTouchEvent(event);

		if (!isScaling) {

			if (mDetector != null) {
				mDetector.onTouchEvent(event);
			}

			final int action = event.getAction() & MotionEvent.ACTION_MASK;

			switch (action) {

			case MotionEvent.ACTION_DOWN:
				if (callback != null)
					callback.onTouchDown();
				savedMatrix.set(matrix);
				startPoint.set(event.getX(), event.getY());
				mode = DRAG;
				isMovingUp = false;
				break;

			case MotionEvent.ACTION_UP:
				if (mode != ZOOM) {
					checkReset();
				} else {

					float scale = mScaleFactor;
					if (mScaleFactor >= maxScale || mScaleFactor <= minScale) {
						mScaleFactor = (mScaleFactor >= maxScale ? maxScale : minScale);
					}

					float to_x = (canvasWidth - scale * bitmapWidth) / 2;
					float to_y = (canvasHeight - scale * bitmapHeight) / 2;

					boolean isWrongLocation = (to_x != mPosX || to_y != mPosY);

					System.out.println("minScale: " + minScale + ",  mScaleFactor: " + mScaleFactor);
					// mean need small or bigger
					if (mScaleFactor <= minScale) {
						System.out.println("!!!!!!");
						updatePosition();
						initScaleAnimation(scale, mScaleFactor);
						if (isWrongLocation) {
							initTranslateAnimation(mPosX, to_x, mPosY, to_y, SMALL_DURATION, false);
							Log.i(LOG_TAG, "anim:" + mPosX + "->" + to_x + "," + mPosY + "->" + to_y);
							mTranslateAnimation.setAnimationListener(mAnimationListener);
							mTranslateAnimation.start();
						} else {
							mScaleAnimation.start();
						}
						savedMatrix.reset();
						savedMatrix.set(matrix);

						invalidate();
					}

				}

				mode = NONE;
				isMovingUp = false;
				break;

			case MotionEvent.ACTION_MOVE:
//				if (mode == DRAG) {
//					// if (mScaleFactor != defaultScale)
//					{
//						matrix.set(savedMatrix);
//						final int pointerCount = event.getPointerCount();
//
//						for (int p = 0; p < pointerCount; p++) {
//							updatePosition();
//							float move_x = event.getX(p) - startPoint.x;
//							float move_y = event.getY(p) - startPoint.y;
//
//							if (mScaleFactor == minScale) {
//								move_y = 0;
//							}
//							checkMove(move_x, move_y);
//						}
//						isMovingUp = true;
//					}
//				}
				break;
			}
		}

		return true;
	}

	private void updatePosition() {
		src[0] = src[1] = 0;
		matrix.mapPoints(dst, src);
		mPosX = dst[0];
		mPosY = dst[1];
	}

//	private void checkReset2() {
//		if (/* mScaleFactor == defaultScale || */!isMovingUp)
//			return;
//
//		updatePosition();
//
//		float tw = mScaleFactor * bitmapWidth, th = mScaleFactor * bitmapHeight;
//
//		float tx = mPosX, ty = mPosY;
//		float abs_dh = Math.abs(canvasHeight - th);
//		float dw = canvasWidth - tw;
//
//		if (th < canvasHeight) {
//			ty = abs_dh / 2;
//		} else if (mPosY > 0) {
//			ty = 0;
//			// ty = mPosY > 0 ? 0 : -abs_dh;
//		} else if (mPosY < -abs_dh) {
//			ty = -abs_dh;
//		}
//
//		// tw always bigger than canvas width
//
//		// System.out.println("  mPosX " + mPosX + "  ,dw " + dw);
//
////		if (mPosX > 0) {
////			tx = 0;
////		} else if (mPosX < dw) {
////			tx = dw;
////		}
//
//		if (tw <= canvasWidth) {
//			tx = (canvasWidth - tw) / 2;
//		} else if (mPosX > 0) {
//			tx = 0;
//		} else if (mPosX < dw) {
//			tx = dw;
//		}
//
//		if (mPosX > 250) {
////			TranslateAnimation trans = new TranslateAnimation(-200, 0, 0, 0);
////			trans.setDuration(500);
////			this.startAnimation(trans);
//			if (callback != null)
//				callback.onFlingPrevious();
//			return;
//		} else if (mPosX < dw - 250) {
////			TranslateAnimation trans = new TranslateAnimation(200, 0, 0, 0);
////			trans.setDuration(500);
////			this.startAnimation(trans);
//			if (callback != null)
//				callback.onFlingNext();
//			return;
//		}
//
//		if (tx != mPosX || ty != mPosY) {
//			initTranslateAnimation(mPosX, tx, mPosY, ty);
//			invalidate();
//		}
//
//	}

	private void checkReset() {
		// if (mScaleFactor == defaultScale || !isMovingUp)

		float scale = mScaleFactor;
		if (mScaleFactor >= maxScale || mScaleFactor <= minScale) {
			mScaleFactor = (mScaleFactor >= maxScale ? maxScale : minScale);
		}

		updatePosition();

		float to_x = (canvasWidth - scale * bitmapWidth) / 2;
		float to_y = (canvasHeight - scale * bitmapHeight) / 2;

		boolean isWrongLocation = (to_x != mPosX || to_y != mPosY);

		Log.i(LOG_TAG, "scale end:min:" + minScale + ",scale:" + scale + ",new:" + mScaleFactor + ",iswr:"
				+ isWrongLocation);

		// mean need small or bigger
		if (scale != mScaleFactor) {
			initScaleAnimation(scale, mScaleFactor);
			if (isWrongLocation) {
				initTranslateAnimation(mPosX, to_x, mPosY, to_y, SMALL_DURATION, false);
				Log.i(LOG_TAG, "anim:" + mPosX + "->" + to_x + "," + mPosY + "->" + to_y);
				mTranslateAnimation.setAnimationListener(mAnimationListener);
				mTranslateAnimation.start();
			} else {
				mScaleAnimation.start();
			}
		} else {
			Log.i(LOG_TAG, "mPosX:" + mPosX + ", mPosY:" + mPosY + "; canvasWidth:" + canvasWidth);
			float tw = mScaleFactor * bitmapWidth, th = mScaleFactor * bitmapHeight;

			float tx = mPosX, ty = mPosY;
			float abs_dh = Math.abs(canvasHeight - th);
			float dw = canvasWidth - tw;

			if (th < canvasHeight) {
				ty = abs_dh / 2;
			} else if (mPosY > 0) {
				ty = 0;
				// ty = mPosY > 0 ? 0 : -abs_dh;
			} else if (mPosY < -abs_dh) {
				ty = -abs_dh;
			}

			if (tw <= canvasWidth) {
				tx = (canvasWidth - tw) / 2;
			} else if (mPosX > 0) {
				tx = 0;
			} else if (mPosX < dw) {
				tx = dw;
			}

			// if (mPosX > 250) {
			if (mPosX > canvasWidth / 2) {

//				TranslateAnimation trans = new TranslateAnimation(-200, 0, 0, 0);
//				trans.setDuration(500);
//				this.startAnimation(trans);
				if (callback != null)
					callback.onFlingPrevious();
				return;
				// } else if (mPosX < dw - 250) {
			} else if (mPosX < dw - canvasWidth / 2) {
//				TranslateAnimation trans = new TranslateAnimation(200, 0, 0, 0);
//				trans.setDuration(500);
//				this.startAnimation(trans);
				if (callback != null)
					callback.onFlingNext();
				return;
			}

			if (tx != mPosX || ty != mPosY) {
				initTranslateAnimation(mPosX, tx, mPosY, ty);
				invalidate();
			}
		}

		savedMatrix.reset();
		savedMatrix.set(matrix);

		invalidate();

	}

	/**
	 * need add left and right go fun
	 * 
	 * @param move_x
	 * @param move_y
	 */
	private void checkMove(float move_x, float move_y) {
		// float npx = mPosX + move_x;

		// if (npx >= leftX && npx <= rightX)
		{
			matrix.postTranslate(move_x, move_y);
			invalidate();
		}
	}

	private void initTranslateAnimation(float fromx, float tox, float fromy, float toy, long duration,
			boolean autoStart) {
		if (mTranslateAnimation == null) {
			mTranslateAnimation = new AbsoluteTranslateAnimation(fromx, tox, fromy, toy);
			mTranslateAnimation.setInterpolator(new DecelerateInterpolator(0.515f));
		} else {
			mTranslateAnimation.reset();
			mTranslateAnimation.setTransformation(fromx, tox, fromy, toy);
		}
		mTranslateAnimation.setDuration(duration);
		isScaleAnimCanStarted = false;
		mTranslateAnimation.setAnimationListener(null);
		if (autoStart) {
			mTranslateAnimation.start();
		}
	}

	private void initTranslateAnimation(float fromx, float tox, float fromy, float toy, long duration) {
		initTranslateAnimation(fromx, tox, fromy, toy, duration, true);
	}

	private void initTranslateAnimation(float fromx, float tox, float fromy, float toy) {
		initTranslateAnimation(fromx, tox, fromy, toy, DEFAULT_DURATION);
	}

	private void initScaleAnimation(float fromScaleFactor, float toScaleFactor) {
		if (mScaleAnimation == null) {
			mScaleAnimation = new AbsoluteScaleAnimation(fromScaleFactor, toScaleFactor, fromScaleFactor,
					toScaleFactor);
			mScaleAnimation.setInterpolator(new DecelerateInterpolator(0.515f));
			mScaleAnimation.setDuration(DEFAULT_DURATION);
		} else {
			mScaleAnimation.reset();
			mScaleAnimation.setScaleformation(fromScaleFactor, toScaleFactor, fromScaleFactor, toScaleFactor);
		}
		isScaleAnimCanStarted = false;
	}

	private void updateAnimation() {
		if (isTranslateAnimationOk()) {
			float[] values = new float[2];
			mTranslateAnimation.getCurrentPostTranslate(values);
			matrix.postTranslate(values[0], values[1]);
		}
		if (isScaleAnimationOk()) {
			float[] scales = new float[2];
			mScaleAnimation.getCurrentScale(scales);
			matrix.postScale(scales[0], scales[1], canvasWidth / 2, canvasHeight / 2);
		}
	}

	private boolean isTranslateAnimationOk() {
		return mTranslateAnimation != null && !mTranslateAnimation.hasEnded();
	}

	private boolean isScaleAnimationOk() {
		return isScaleAnimCanStarted && mScaleAnimation != null && !mScaleAnimation.hasEnded();
	}

	private void checkAnimation() {
		if (isTranslateAnimationOk() || isScaleAnimationOk()) {
			invalidate();
		}
	}

	private void updateMoveArea() {
		leftX = -mScaleFactor * bitmapWidth - MAX_MOVE_P_X;
		rightX = MAX_MOVE_P_X;
	}

	public boolean onScale(ScaleGestureDetector detector) {
		float scale = detector.getScaleFactor();
		float scale_cp = 0;
		if (Float.isInfinite(scale) || Float.isNaN(scale))
			return true;

		scale_cp = mScaleFactor * scale;
		if (scale_cp >= maxScale + 0.2f || scale_cp <= minScale - 0.2f) {
			Log.i(LOG_TAG, "c:" + scale_cp + ",r:" + mScaleFactor);
			return true;
		}
		mScaleFactor = scale_cp;
		matrix.postScale(scale, scale, detector.getFocusX(), detector.getFocusY());

		invalidate();
		return true;
	}

	public boolean onScaleBegin(ScaleGestureDetector detector) {
		if (isError)
			return false;
		isScaling = true;
		mode = ZOOM;
		return true;
	}

	@Override
	public void onScaleEnd(ScaleGestureDetector detector) {
		isScaling = false;

		// checkScale();
		// updateMoveArea();
	}

	private void checkScale() {
		float scale = mScaleFactor;
		if (mScaleFactor >= maxScale || mScaleFactor <= minScale) {
			mScaleFactor = (mScaleFactor >= maxScale ? maxScale : minScale);
		}

		updatePosition();

		float to_x = (canvasWidth - scale * bitmapWidth) / 2;
		float to_y = (canvasHeight - scale * bitmapHeight) / 2;

		boolean isWrongLocation = (to_x != mPosX || to_y != mPosY);

		// mean need small or bigger
		if (scale != mScaleFactor) {
			initScaleAnimation(scale, mScaleFactor);
			if (isWrongLocation) {
				initTranslateAnimation(mPosX, to_x, mPosY, to_y, SMALL_DURATION);
				mTranslateAnimation.setAnimationListener(mAnimationListener);
			} else {
				mScaleAnimation.start();
			}
		} else if (isWrongLocation) {

			if (mPosX + scale * bitmapWidth > canvasWidth && mPosX < 0) {
				to_x = mPosX;
			}

			if (mPosY + scale * bitmapHeight > canvasHeight && mPosY < 0) {
				to_y = mPosY;
			}

			initTranslateAnimation(mPosX, to_x, mPosY, to_y);
		}

		savedMatrix.reset();
		savedMatrix.set(matrix);

		invalidate();
	}

	public boolean onDown(MotionEvent e) {
		return false;
	}

	private long lastTime = 0;

	public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
//		System.out.println("  onFling mPosX: " + mPosX);
//		if (SystemClock.uptimeMillis() - lastTime < 500) {
//			return true;
//		}
//		lastTime = SystemClock.uptimeMillis();
//
//		float tw = mScaleFactor * bitmapWidth;
//
//		float x = e2.getX() - e1.getX();
//		float y = e2.getY() - e1.getY();
//		float x_limit = screen.widthPixels / 2;
//		float x_abs = Math.abs(x);
//		float y_abs = Math.abs(y);
//		// if (x_abs >= y_abs) {
//		if (mPosX == 0) {
//			// if (x > x_limit || x < -x_limit) {
////			if (x > 0) {
////
////				TranslateAnimation trans = new TranslateAnimation(-x, 0, -y, 0);
////				trans.setDuration(300);
////				this.startAnimation(trans);
////				if (callback != null)
////					callback.onFlingPrevious();
////			} else if (x <= 0) {
//
//			TranslateAnimation trans = new TranslateAnimation(-x, 0, -y, 0);
//			trans.setDuration(500);
//			this.startAnimation(trans);
//			if (callback != null)
//				callback.onFlingNext();
////			}
//			// }
//		} else if (mPosX == screen.widthPixels) {
//			TranslateAnimation trans = new TranslateAnimation(-x, 0, -y, 0);
//			trans.setDuration(300);
//			this.startAnimation(trans);
//			if (callback != null)
//				callback.onFlingPrevious();
//		}

		return true;
	}

	public void onLongPress(MotionEvent e) {

	}

	public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
		return false;
	}

	public void onShowPress(MotionEvent e) {

	}

	public boolean onSingleTapUp(MotionEvent e) {
		return false;
	}

	class TranslateAnimationListener implements AnimationListener {

		@Override
		public void onAnimationEnd(Animation animation) {
			if (mScaleAnimation != null) {
				isScaleAnimCanStarted = true;
				mScaleAnimation.start();
				postInvalidate();
			}
		}

		@Override
		public void onAnimationRepeat(Animation animation) {

		}

		@Override
		public void onAnimationStart(Animation animation) {

		}

	}

	public boolean onDoubleTap(MotionEvent e) {
		if (isError)
			return false;
		Log.i(LOG_TAG, "onDoubleTap:" + defaultScale + "," + maxScale + ",current:" + mScaleFactor);
		float finalFactor = mScaleFactor;
		if (mScaleFactor > minScale) {
			finalFactor = minScale;
		} else if (mScaleFactor == minScale) {
			finalFactor = maxScale;
		}

		if (finalFactor == maxScale) {
			float scale = finalFactor / mScaleFactor;
			mScaleFactor = finalFactor;
			matrix.postScale(scale, scale, e.getX(), e.getY());

			invalidate();
		} else {
			updateView();
		}
		return false;
	}

	@Override
	public boolean onDoubleTapEvent(MotionEvent e) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean onSingleTapConfirmed(MotionEvent e) {
		// TODO Auto-generated method stub
		return false;
	}

	public void setCallback(MyCallback myCallback) {
		this.callback = myCallback;
	}

	public interface MyCallback {

		public void onTouchDown();

		public void onFlingNext();

		public void onFlingPrevious();
	}
}
